﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;

namespace LightSystemTest
{
    public class LightRenderer
    {
        public List<SpotLight> spotLights;
        public List<PointLight> pointLights;

        public GraphicsDeviceManager graphics;
        public SpriteBatch spriteBatch;

        public RenderTarget2D backBufferCache;
        public RenderTarget2D midGroundTarget;
        public RenderTarget2D shadowCastersTarget;
        public RenderTarget2D unwrapTarget;
        public RenderTarget2D occlusionMap;
        public RenderTarget2D postProcessTarget;
        public RenderTarget2D horizontalBlurTarget;
        public RenderTarget2D verticalBlurTarget;
        public RenderTarget2D lightMap;

        public BlendState collapseBlendState;
        public SamplerState sampleState;

        public float lightBias = -1f;
        public float minLight = -1f;

        public Effect verticalize;
        public Effect verticalizeSpotlight;
        public Effect pointLight;
        public Effect spotLight;
        public Effect lightBlend;
        public Effect verticalBlur;
        public Effect horizontalBlur;

        Rectangle fullScreen;
        Vector2 screenDims;

        public LightRenderer(GraphicsDeviceManager _graphics)
        {
            graphics = _graphics;
        }

        public void Initialize()
        {
            spotLights = new List<SpotLight>();
            pointLights = new List<PointLight>();

            backBufferCache = new RenderTarget2D(graphics.GraphicsDevice, graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Height);
            midGroundTarget = new RenderTarget2D(graphics.GraphicsDevice, graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Height);
            shadowCastersTarget = new RenderTarget2D(graphics.GraphicsDevice, graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Height);
            unwrapTarget = new RenderTarget2D(graphics.GraphicsDevice, graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Width, false, SurfaceFormat.HdrBlendable, DepthFormat.None);
            occlusionMap = new RenderTarget2D(graphics.GraphicsDevice, graphics.GraphicsDevice.Viewport.Width, 1, false, SurfaceFormat.HdrBlendable, DepthFormat.None);
            postProcessTarget = new RenderTarget2D(graphics.GraphicsDevice, graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Height);
            horizontalBlurTarget = new RenderTarget2D(graphics.GraphicsDevice, graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Height);
            verticalBlurTarget = new RenderTarget2D(graphics.GraphicsDevice, graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Height);
            lightMap = new RenderTarget2D(graphics.GraphicsDevice, graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Height, false, SurfaceFormat.Color, DepthFormat.None, 1, RenderTargetUsage.PreserveContents);

            collapseBlendState = new BlendState();
            collapseBlendState.ColorBlendFunction = BlendFunction.Min;
            collapseBlendState.AlphaBlendFunction = BlendFunction.Min;
            collapseBlendState.ColorSourceBlend = Blend.One;
            collapseBlendState.ColorDestinationBlend = Blend.One;
            collapseBlendState.AlphaSourceBlend = Blend.One;
            collapseBlendState.AlphaDestinationBlend = Blend.One;
            sampleState = new SamplerState();
            sampleState.Filter = TextureFilter.Point;
            sampleState.AddressU = TextureAddressMode.Clamp;
            sampleState.AddressV = TextureAddressMode.Clamp;

            screenDims = new Vector2(graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Height);
            fullScreen = new Rectangle(0, 0, graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Height);
        }

        public void LoadContent(ContentManager Content)
        {
            spriteBatch = new SpriteBatch(graphics.GraphicsDevice);

            verticalize = Content.Load<Effect>(@"Effects\Verticalize");
            verticalizeSpotlight = Content.Load<Effect>(@"Effects\VerticalizeSpotlight");
            pointLight = Content.Load<Effect>(@"Effects\PointLightFinal");
            spotLight = Content.Load<Effect>(@"Effects\SpotLightFinal");
            lightBlend = Content.Load<Effect>(@"Effects\Finalize");
            verticalBlur = Content.Load<Effect>(@"Effects\VerticalBlur");
            horizontalBlur = Content.Load<Effect>(@"Effects\HorizontalBlur");
        }

        public void BeginDrawBackground()
        {
            graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
            graphics.GraphicsDevice.SetRenderTarget(backBufferCache);
        }

        public void EndDrawBackground()
        {
        }

        public void BeginDrawShadowCasters()
        {
            graphics.GraphicsDevice.SetRenderTarget(midGroundTarget);
            graphics.GraphicsDevice.Clear(Color.Transparent);
        }

        public void EndDrawShadowCasters()
        {
            graphics.GraphicsDevice.SetRenderTarget(shadowCastersTarget);
            graphics.GraphicsDevice.Clear(Color.Transparent);

            spriteBatch.Begin();
            spriteBatch.Draw(midGroundTarget, new Rectangle(0, 0, graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Height), Color.Black);
            spriteBatch.End();
        }

        public void DrawLitScene()
        {
            if (lightBias < 0)
            {
                //error
                return;
            }
            if (minLight < 0)
            {
                //error
                return;
            }

            PrepareResources();

            for (int i = 0; i < spotLights.Count; i++)
            {
                float lightDirAngle = Vector2.Dot(spotLights[i].direction, Vector2.UnitY);
                lightDirAngle = (float)(Math.Acos(lightDirAngle) * Math.Sign(spotLights[i].direction.X));

                if (float.IsNaN(lightDirAngle))
                    lightDirAngle = ((Math.Sign(-spotLights[i].direction.Y) + 1) / 2f) * (float)Math.PI;

                float angleBias = spotLights[i].GetAngleBias();
                lightDirAngle += (Math.Abs(Math.Sign(lightDirAngle))) * (angleBias * (1 - ((Math.Sign(lightDirAngle) + 1) / 2f)));

                UnwrapShadowCasters(spotLights[i], lightDirAngle, angleBias);

                CreateOcclusionMap();

                CreateLightMap(spotLights[i], lightDirAngle, angleBias);

                BlurLightMaps(spotLights[i]);

                AccumulateLightMaps(spotLights[i]);
            }

            for (int i = 0; i < pointLights.Count; i++)
            {
                UnwrapShadowCasters(pointLights[i]);

                CreateOcclusionMap();

                CreateLightMap(pointLights[i]);

                BlurLightMaps(pointLights[i]);

                AccumulateLightMaps(pointLights[i]);
            }

            RenderFinalScene();
        }

        private void PrepareResources()
        {
            verticalizeSpotlight.Parameters["TargetWidth"].SetValue(screenDims.X);
            verticalizeSpotlight.Parameters["TargetHeight"].SetValue(screenDims.Y);
            verticalizeSpotlight.Parameters["DiagonalLength"].SetValue(screenDims.Length());

            spotLight.Parameters["ScreenDimensions"].SetValue(screenDims);
            spotLight.Parameters["DiagonalLength"].SetValue(screenDims.Length());
            spotLight.Parameters["Bias"].SetValue(lightBias);

            horizontalBlur.Parameters["ScreenDims"].SetValue(screenDims);
            verticalBlur.Parameters["ScreenDims"].SetValue(screenDims);

            verticalize.Parameters["TargetWidth"].SetValue(screenDims.X);
            verticalize.Parameters["TargetHeight"].SetValue(screenDims.Y);
            verticalize.Parameters["DiagonalLength"].SetValue(screenDims.Length());

            pointLight.Parameters["ScreenDimensions"].SetValue(screenDims);
            pointLight.Parameters["DiagonalLength"].SetValue(screenDims.Length());
            pointLight.Parameters["Bias"].SetValue(lightBias);

            graphics.GraphicsDevice.SetRenderTarget(lightMap);
            graphics.GraphicsDevice.Clear(Color.Black);
        }

        private void UnwrapShadowCasters(SpotLight sLight, float lightDirAngle, float angleBias)
        {
            graphics.GraphicsDevice.SetRenderTarget(unwrapTarget);
            graphics.GraphicsDevice.Clear(Color.Transparent);

            verticalizeSpotlight.Parameters["LightPos"].SetValue(sLight.Position);
            verticalizeSpotlight.Parameters["MinAngle"].SetValue(lightDirAngle - (sLight.outerAngle / 2f));
            verticalizeSpotlight.Parameters["MaxAngle"].SetValue(lightDirAngle + (sLight.outerAngle / 2f));
            verticalizeSpotlight.Parameters["ArcSize"].SetValue(sLight.outerAngle);

            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, sampleState, null, null, verticalizeSpotlight);
            spriteBatch.Draw(shadowCastersTarget, new Rectangle(0, 0, fullScreen.Width, fullScreen.Width), Color.White);
            spriteBatch.End();
        }

        private void UnwrapShadowCasters(PointLight pLight)
        {
            graphics.GraphicsDevice.SetRenderTarget(unwrapTarget);
            graphics.GraphicsDevice.Clear(Color.Transparent);

            verticalize.Parameters["LightPos"].SetValue(pLight.Position);

            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, sampleState, null, null, verticalize);
            spriteBatch.Draw(shadowCastersTarget, new Rectangle(0, 0, fullScreen.Width, fullScreen.Width), Color.White);
            spriteBatch.End();
        }

        private void CreateOcclusionMap()
        {
            graphics.GraphicsDevice.SetRenderTarget(occlusionMap);
            graphics.GraphicsDevice.Clear(Color.White);
            spriteBatch.Begin(SpriteSortMode.Deferred, collapseBlendState, sampleState, null, null);
            for (int j = 0; j < fullScreen.Width; j++)
            {
                spriteBatch.Draw(unwrapTarget, new Rectangle(0, 0, graphics.GraphicsDevice.Viewport.Width, 1), new Rectangle(0, j, graphics.GraphicsDevice.Viewport.Width, 1), Color.White);
            }
            spriteBatch.End();
        }

        private void CreateLightMap(SpotLight sLight, float lightDirAngle, float angleBias)
        {
            graphics.GraphicsDevice.SetRenderTarget(postProcessTarget);
            graphics.GraphicsDevice.Clear(Color.Black);

            spotLight.Parameters["LightPos"].SetValue(sLight.Position);
            spotLight.Parameters["LightPow"].SetValue(sLight.Power);
            spotLight.Parameters["Radius"].SetValue(sLight.radius);
            spotLight.Parameters["OuterMinAngle"].SetValue(lightDirAngle - (sLight.outerAngle / 2f));
            spotLight.Parameters["OuterMaxAngle"].SetValue(lightDirAngle + (sLight.outerAngle / 2f));
            spotLight.Parameters["CenterAngle"].SetValue(lightDirAngle);
            spotLight.Parameters["HalfInnerArc"].SetValue(sLight.innerAngle / 2f);
            spotLight.Parameters["HalfOuterArc"].SetValue(sLight.outerAngle / 2f);
            spotLight.Parameters["AngleBias"].SetValue(angleBias);

            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, sampleState, null, null, spotLight);
            spriteBatch.Draw(occlusionMap, fullScreen, Color.White);
            spriteBatch.End();
        }

        private void CreateLightMap(PointLight pLight)
        {
            graphics.GraphicsDevice.SetRenderTarget(postProcessTarget);
            graphics.GraphicsDevice.Clear(Color.Black);

            pointLight.Parameters["LightPos"].SetValue(pLight.Position);
            pointLight.Parameters["LightPow"].SetValue(pLight.Power);
            pointLight.Parameters["Radius"].SetValue(pLight.radius);

            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, sampleState, null, null, pointLight);
            spriteBatch.Draw(occlusionMap, fullScreen, Color.White);
            spriteBatch.End();
        }

        private void BlurLightMaps(Light light)
        {
            graphics.GraphicsDevice.SetRenderTarget(horizontalBlurTarget);
            graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

            horizontalBlur.Parameters["LightPos"].SetValue(light.Position);
            horizontalBlur.Parameters["Radius"].SetValue(light.radius);

            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, null, null, null, horizontalBlur);
            spriteBatch.Draw(postProcessTarget, fullScreen, Color.White);
            spriteBatch.End();

            graphics.GraphicsDevice.SetRenderTarget(verticalBlurTarget);
            graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

            verticalBlur.Parameters["LightPos"].SetValue(light.Position);
            verticalBlur.Parameters["Radius"].SetValue(light.radius);

            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, null, null, null, verticalBlur);
            spriteBatch.Draw(horizontalBlurTarget, fullScreen, Color.White);
            spriteBatch.End();
        }

        private void AccumulateLightMaps(Light light)
        {
            graphics.GraphicsDevice.SetRenderTarget(lightMap);

            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, null, null, null);
            spriteBatch.Draw(verticalBlurTarget, fullScreen, light.color);
            spriteBatch.End();
        }

        private void RenderFinalScene()
        {
            graphics.GraphicsDevice.SetRenderTarget(null);
            graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

            graphics.GraphicsDevice.Textures[1] = lightMap;
            lightBlend.Parameters["MinLight"].SetValue(minLight);

            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, null, null, null, lightBlend);
            spriteBatch.Draw(backBufferCache, fullScreen, Color.White);
            spriteBatch.End();
            graphics.GraphicsDevice.Textures[1] = null;

            spriteBatch.Begin();
            spriteBatch.Draw(midGroundTarget, fullScreen, Color.White);
            spriteBatch.End();
        }
    }
}
